/*
 * Copyright (c) 2001 Sun Microsystems, Inc.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *       Sun Microsystems, Inc. for Project JXTA."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
 *    not be used to endorse or promote products derived from this
 *    software without prior written permission. For written
 *    permission, please contact Project JXTA at http://www.jxta.org.
 *
 * 5. Products derived from this software may not be called "JXTA",
 *    nor may "JXTA" appear in their name, without prior written
 *    permission of Sun.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL SUN MICROSYSTEMS OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of Project JXTA.  For more
 * information on Project JXTA, please see
 * <http://www.jxta.org/>.
 *
 * This license is based on the BSD license adopted by the Apache Foundation.
 *
 * $Id: LimitInputStream.java,v 1.1 2005/05/03 06:44:48 hamada Exp $
 */

package net.jxta.util;

import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.InputStream;

import java.io.IOException;

import org.apache.log4j.Logger;
import org.apache.log4j.Level;

/**
 *  Implements a bounds on the number of bytes which may be read from an
 *  InputStream. {link LimitInputStream.close() close()} does not close the
 *  underlying stream.
 **/
public class LimitInputStream extends FilterInputStream {
    
    /*
     *  Log4J Catagory
     */
    private static final Logger LOG = Logger.getLogger( LimitInputStream.class.getName() );
    
    private transient long limit;
    
    private transient long read;
    
    private transient long mark;
    
    private transient boolean fatalUnderflow;
    
    private transient boolean alreadycounting;
    
    /**
     * Creates a new instance of LimitInputStream
     *
     *    @param  in  the stream which will be limited.
     *    @param  limit   the number of bytes which can be read from the stream.
     **/
    public LimitInputStream( InputStream in, long limit ) {
        this( in, limit, false );
    }
    
    /**
     * Creates a new instance of LimitInputStream
     *
     *    @param  in  the stream which will be limited.
     *    @param  limit   the number of bytes which can be read from the stream.
     *    @param  underflowThrows if the underlying stream EOFs before limit then
     *    an IOException will be thrown.
     **/
    public LimitInputStream( InputStream in, long limit, boolean underflowThrows ) {
        super(in);
        this.limit = limit;
        this.mark = -1;
        this.read = 0;
        this.fatalUnderflow = underflowThrows;
        this.alreadycounting = false;
    }
    
    /**
     * {@inheritDoc}
     *
     *  <p/>Debugging toString.
     **/
    public String toString() {
        if( null == in ) {
            return "closed/" + super.toString();
        } else {
            if( in instanceof ByteArrayInputStream ) {
                // ByteArrayInputStream.toString() prints the entire stream! 
                return in.getClass().getName() + "@" + System.identityHashCode(in)  + "/" + super.toString() + ":" + read + "-" + limit;
            } else {
                return in.toString() + "/" + super.toString() + ":" + read + "-" + limit;
            }
        }
    }
    
    /**
     * Closes this input stream and releases any system resources
     * associated with the stream.
     * <p/>
     * This method simply forgets the underlying stream.
     *
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public void close() throws IOException {
        in = null;
    }
    
    /**
     * Returns the number of bytes that can be read from this input
     * stream without blocking.
     * <p>
     * This method
     * simply performs <code>in.available(n)</code> and
     * returns the result.
     *
     * @return     the number of bytes that can be read from the input stream
     *             without blocking.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public int available() throws IOException {
        if( null == in )
            throw new IOException( "Stream has been closed." );
        
        return (int) Math.min( super.available(), (limit - read) );
    }
    
    /**
     * Marks the current position in this input stream. A subsequent
     * call to the <code>reset</code> method repositions this stream at
     * the last marked position so that subsequent reads re-read the same bytes.
     * <p>
     * The <code>readlimit</code> argument tells this input stream to
     * allow that many bytes to be read before the mark position gets
     * invalidated.
     * <p>
     * This method simply performs <code>in.mark(readlimit)</code>.
     *
     * @param   readlimit   the maximum limit of bytes that can be read before
     *                      the mark position becomes invalid.
     * @see     java.io.FilterInputStream#in
     * @see     java.io.FilterInputStream#reset()
     */
    public void mark(int readlimit) {
        if( null == in )
            return; // don't throw exception to be consistent with other impls.
        
        super.mark( readlimit );
        mark = read;
    }
    
    /**
     * Repositions this stream to the position at the time the
     * <code>mark</code> method was last called on this input stream.
     * <p>
     * This method simply performs <code>in.reset()</code>.
     * <p>
     * Stream marks are intended to be used in
     * situations where you need to read ahead a little to see what's in
     * the stream. Often this is most easily done by invoking some
     * general parser. If the stream is of the type handled by the
     * parse, it just chugs along happily. If the stream is not of
     * that type, the parser should toss an exception when it fails.
     * If this happens within readlimit bytes, it allows the outer
     * code to reset the stream and try another parser.
     *
     * @exception  IOException  if the stream has not been marked or if the
     *               mark has been invalidated.
     * @see        java.io.FilterInputStream#in
     * @see        java.io.FilterInputStream#mark(int)
     */
    public void reset() throws IOException {
        if( null == in )
            throw new IOException( "Stream has been closed." );
        
        if( -1 == mark )
            throw new IOException( "reset() without mark(), or I dont know where mark is" );
        
        super.reset();
        
        read = mark;
    }
    
    /**
     * Skips over and discards <code>n</code> bytes of data from the
     * input stream. The <code>skip</code> method may, for a variety of
     * reasons, end up skipping over some smaller number of bytes,
     * possibly <code>0</code>. The actual number of bytes skipped is
     * returned.
     * <p>
     * This method
     * simply performs <code>in.skip(n)</code>.
     *
     * @param      n   the number of bytes to be skipped.
     * @return     the actual number of bytes skipped.
     * @exception  IOException  if an I/O error occurs.
     */
    public synchronized long skip(long n) throws IOException {
        if( null == in )
            throw new IOException( "Stream has been closed." );
        
        long skipLen = Math.min( n, (limit - read) );
        
        boolean wascounting = alreadycounting;
        alreadycounting = true;
        long result = super.skip( skipLen );
        alreadycounting = wascounting;
        
        if( (-1 != result) && !alreadycounting )
            read += result;
        
        return result;
    }
    
    /**
     * Reads the next byte of data from this input stream. The value
     * byte is returned as an <code>int</code> in the range
     * <code>0</code> to <code>255</code>. If no byte is available
     * because the end of the stream has been reached, the value
     * <code>-1</code> is returned. This method blocks until input data
     * is available, the end of the stream is detected, or an exception
     * is thrown.
     * <p>
     * This method
     * simply performs <code>in.read()</code> and returns the result.
     *
     * @return     the next byte of data, or <code>-1</code> if the end of the
     *             stream is reached.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public synchronized int read() throws IOException {
        if( null == in )
            throw new IOException( "Stream has been closed." );
        
        if ( read >= limit )
            return -1;
        
        boolean wascounting = alreadycounting;
        alreadycounting = true;
        int result = super.read();
        alreadycounting = wascounting;
        
        if( !alreadycounting ) {
            if( -1 != result ) {
                read++;
            } else {
                if ( fatalUnderflow && (read != limit) ) {
                    IOException failed = new IOException( "Underflow in read, stream EOFed at " + read + " before limit of " + limit );
                    if (LOG.isEnabledFor(Level.WARN))
                        LOG.warn( failed, failed );
                    throw failed;
                }
            }
        }
        
        return result;
    }
    
    /**
     * Reads up to <code>len</code> bytes of data from this input stream
     * into an array of bytes. This method blocks until some input is
     * available.
     * <p>
     * This method simply performs <code>in.read(b, off, len)</code>
     * and returns the result.
     *
     * @param      b     the buffer into which the data is read.
     * @param      off   the start offset of the data.
     * @param      len   the maximum number of bytes read.
     * @return     the total number of bytes read into the buffer, or
     *             <code>-1</code> if there is no more data because the end of
     *             the stream has been reached.
     * @exception  IOException  if an I/O error occurs.
     * @see        java.io.FilterInputStream#in
     */
    public synchronized int read(byte[] b, int off, int len) throws IOException {
        if( null == in )
            throw new IOException( "Stream has been closed." );
        
        if( read >= limit ) {
            return -1;
        }
        
        int readLen = (int) Math.min( len, limit - read );
        
        boolean wascounting = alreadycounting;
        alreadycounting = true;
        int result = super.read( b, off, readLen );
        alreadycounting = wascounting;
        
        if( !alreadycounting ) {
            if( -1 != result )
                read += result;
            else {
                if ( fatalUnderflow && (read != limit) ) {
                    IOException failed = new IOException( "Underflow while tring to read " + readLen + ", stream EOFed at " + read + " before limit of " + limit );
                    if (LOG.isEnabledFor(Level.WARN))
                        LOG.warn( failed, failed );
                    throw failed;

                }
            }
        }
        
        return result;
    }
}
